package api

import (
	"context"
	"log"
	"net"
	"strconv"
	"sync"

	"gitee.com/dingiyan/goMyAdmin/db"
	"gitee.com/wenj91/sqls/dialect"
	"gitee.com/wenj91/sqls/pkg/config"
	"gitee.com/wenj91/sqls/pkg/database"
	"gitee.com/wenj91/sqls/pkg/handler"
	"gitee.com/wenj91/sqls/pkg/lsp"
	"github.com/gin-gonic/gin"
	"github.com/sourcegraph/jsonrpc2"
)

const testFileURI = "file:///Users/octref/Code/css-test/test.sql"

// CachedConnection represents a cached LSP connection.
type CachedConnection struct {
	LSPConn  *jsonrpc2.Conn // JSON-RPC connection
	UpdateAt int64          // Data source update timestamp
}

var (
	connections = make(map[string]*CachedConnection) // Cache map with key as "dsId_databaseName"
	rw          sync.RWMutex                         // Read-write lock
)

// GetLSPConnection retrieves or creates an LSP connection for the given dsId and databaseName.
func GetLSPConnection(ctx context.Context, dsId uint, databaseName string) (*jsonrpc2.Conn, error) {
	cacheKey := generateCacheKey(dsId, databaseName)

	// Fetch data source information from database
	record, err := db.GetServerById(dsId)
	if err != nil {
		return nil, err
	}

	rw.RLock()
	cachedConn, exists := connections[cacheKey]
	rw.RUnlock()

	// Return cached connection if it exists and is up-to-date
	if exists && cachedConn.UpdateAt == record.UpdateAt {
		return cachedConn.LSPConn, nil
	}

	// Acquire write lock to update cache
	rw.Lock()
	defer rw.Unlock()

	// Double-check if another goroutine already updated the cache
	if cachedConn, exists := connections[cacheKey]; exists && cachedConn.UpdateAt == record.UpdateAt {
		return cachedConn.LSPConn, nil
	}

	// Create a new LSP connection
	conn, err := createLSPConnection(ctx, record, databaseName)
	if err != nil {
		return nil, err
	}

	// Cache the new connection
	connections[cacheKey] = &CachedConnection{
		LSPConn:  conn,
		UpdateAt: record.UpdateAt,
	}
	return conn, nil
}

// generateCacheKey creates a cache key using dsId and databaseName.
func generateCacheKey(dsId uint, databaseName string) string {
	return strconv.Itoa(int(dsId)) + "_" + databaseName
}

// createLSPConnection creates a new LSP connection for the given data source and database.
func createLSPConnection(ctx context.Context, record *db.Server, databaseName string) (*jsonrpc2.Conn, error) {
	cfg := &config.Config{
		Connections: []*database.DBConfig{
			{
				Driver: dialect.DatabaseDriver(record.Driver),
				User:   record.User,
				Passwd: record.Password,
				Proto:  "tcp",
				Host:   record.IP,
				Port:   int(record.Port),
				DBName: databaseName,
			},
		},
	}

	client, server := net.Pipe()
	connServer := jsonrpc2.NewConn(ctx, jsonrpc2.NewBufferedStream(server, jsonrpc2.VSCodeObjectCodec{}), jsonrpc2.HandlerWithError(handler.NewServer().Handle))
	connClient := jsonrpc2.NewConn(ctx, jsonrpc2.NewBufferedStream(client, jsonrpc2.VSCodeObjectCodec{}), jsonrpc2.HandlerWithError(handler.NewServer().Handle))

	err := initializeServer(ctx, cfg, connServer, connClient)
	if err != nil {
		return nil, err
	}

	return connClient, nil
}

// initializeServer initializes the LSP server with the given config.
func initializeServer(ctx context.Context, cfg *config.Config, connServer *jsonrpc2.Conn, connClient *jsonrpc2.Conn) error {
	initParams := lsp.InitializeParams{InitializationOptions: lsp.InitializeOptions{}}
	if err := callJSONRPC(ctx, connClient, "initialize", initParams, nil); err != nil {
		return err
	}

	configParams := lsp.DidChangeConfigurationParams{
		Settings: struct {
			SQLS *config.Config `json:"sqls"`
		}{SQLS: cfg},
	}
	return callJSONRPC(ctx, connClient, "workspace/didChangeConfiguration", configParams, nil)
}

// callJSONRPC makes a JSON-RPC call to the LSP server.
func callJSONRPC(ctx context.Context, conn *jsonrpc2.Conn, method string, params interface{}, result interface{}) error {
	if err := conn.Call(ctx, method, params, result); err != nil {
		log.Printf("JSON-RPC call %s failed: %v", method, err)
		return err
	}
	return nil
}

// LspComplete handles completion requests via LSP.
func LspComplete(c *gin.Context) {
	// 定义结构体接收 JSON 参数
	var request struct {
		DsId         int    `json:"dsId" binding:"required"`
		DatabaseName string `json:"databaseName" binding:"required"`
		Input        string `json:"input" binding:"required"`
		Line         *int   `json:"line" binding:"required"`
		Col          *int   `json:"col" binding:"required"`
	}

	// 解析 JSON 请求体
	if err := c.ShouldBindJSON(&request); err != nil {
		c.JSON(200, resError(2, "Invalid JSON payload, details: "+err.Error()))
		return
	}

	ctx := context.Background()
	conn, err := GetLSPConnection(ctx, uint(request.DsId), request.DatabaseName)
	if err != nil {
		c.JSON(200, resError(3, "获取LSP连接出错！"+err.Error()))
		return
	}

	openParams := lsp.DidOpenTextDocumentParams{
		TextDocument: lsp.TextDocumentItem{
			URI:        testFileURI,
			LanguageID: "sql",
			Version:    0,
			Text:       request.Input,
		},
	}
	if err := callJSONRPC(ctx, conn, "textDocument/didOpen", openParams, nil); err != nil {
		c.JSON(200, resError(4, "Failed to open document, details: "+err.Error()))
		return
	}

	completionParams := lsp.CompletionParams{
		TextDocumentPositionParams: lsp.TextDocumentPositionParams{
			TextDocument: lsp.TextDocumentIdentifier{URI: testFileURI},
			Position:     lsp.Position{Line: *request.Line, Character: *request.Col},
		},
	}
	var completions []lsp.CompletionItem
	if err := callJSONRPC(ctx, conn, "textDocument/completion", completionParams, &completions); err != nil {
		c.JSON(200, resError(5, "Failed to get completions, details: "+err.Error()))
		return
	}

	c.JSON(200, success(completions))
}
